📝 Резюме · 🧾 Транскрипт (формат) · 📄 Оригинал (9.7 KB)
https://blog.jetbrains.com/qodana/2026/06/fix-common-typescript-issues/

Как исправлять типичные TypeScript-проблемы с помощью Qodana

Источник: https://blog.jetbrains.com/qodana/2026/06/fix-common-typescript-issues/

Краткое содержание

Статья объясняет, где ESLint с @typescript-eslint заканчивает свои возможности и зачем нужен Qodana. Принципиальное ограничение ESLint: каждое правило работает в рамках одного файла, поэтому инструмент не может обнаружить проблемы, охватывающие весь проект — неиспользуемые экспорты, распространение any через несколько файлов, дублирование логики. Именно этот пробел закрывает Qodana.

Пять TypeScript-проблем: ESLint vs. Qodana

Распространение implicit any. ESLint ловит явный any через no-explicit-any, но не отслеживает, как any из внешних источников (например, response.json()) распространяется по цепочке вызовов:

async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  const data = await res.json(); // тип: any
  return data.profile.name;     // падает в runtime, если profile === undefined
}
// Исправление: явно типизировать границу
const data: UserResponse = await res.json();

Qodana отслеживает поток any между файлами и сигнализирует при достижении кода, предполагающего конкретную форму данных.

Оператор non-null assertion (!). ESLint-правило no-non-null-assertion флагирует каждый ! без разбора — команды отключают его из-за шума. Qodana группирует non-null assertions в отдельную категорию отчёта, позволяя отличать оправданное использование от «заглушек» без необходимости выбирать между шумным правилом и полным отсутствием контроля.

Floating promises. ESLint-правило @typescript-eslint/no-floating-promises требует включения type-aware линтинга через parserOptions.project — во многих проектах это не настроено для всех файлов, и правило молча не срабатывает:

// Проблема: saveToDatabase не awaited
async function onSubmit(data: FormData) {
  saveToDatabase(data); // Promise<void>, не awaited
  router.push("/success"); // выполняется до завершения сохранения
}
// Исправление:
  await saveToDatabase(data);

Qodana выполняет type-aware анализ по умолчанию для всего проекта без дополнительной настройки.

Неиспользуемые экспорты. noUnusedLocals в tsconfig исключает экспортируемые символы (компилятор предполагает, что их могут импортировать из других файлов). ESLint-плагин import/no-unused-modules существует, но слишком медленный для регулярного использования на крупных проектах. Qodana строит граф ссылок по всей кодовой базе и находит символы, которые никогда не импортируются.

Дублирование логики между файлами. ESLint не имеет нативного обнаружения дубликатов. Qodana выявляет дублированный код в том же проходе анализа, что и типовые проблемы и неиспользуемые экспорты.

Настройка Qodana для TypeScript-проекта

# qodana.yaml
version: "1.0"
linter: jetbrains/qodana-js:2026.1
bootstrap: npm ci
profile:
  name: qodana.recommended
failThreshold: 0
exclude:
  - name: All
    paths:
      - dist
      - node_modules

Если первый запуск выявил сотни существующих проблем, функция baseline фиксирует текущее состояние в qodana.sarif.json. CI начинает падать только на новых нарушениях, а накопленный долг остаётся видимым в отчёте, не блокируя каждый PR.

Значимость

Статья чётко очерчивает архитектурное ограничение файл-ориентированных линтеров и показывает, что анализ уровня проекта — не опциональное дополнение, а необходимость для TypeScript-кодовых баз среднего и большого размера. Baseline-механизм делает внедрение Qodana в существующий проект практически безболезненным.

🧾 Транскрипт (формат)

How To Fix Common TypeScript Issues With Qodana Source: https://blog.jetbrains.com/qodana/2026/06/fix-common-typescript-issues/

Most TypeScript projects already run ESLint with @typescript-eslint. That covers a lot: explicit any, floating promises, non-null assertions, and more. If your linting setup is solid, you’re catching the obvious issues in the editor before code review.

ESLint rules can’t produce cross-file findings. Each rule runs within a single file’s scope, which means ESLint can’t tell you that an export is unused everywhere in the codebase, that an any-typed value from one file is causing unsafe assumptions five files away, or that two components implement the same logic independently. That’s the gap Qodana fills.

Here are five TypeScript issues worth addressing, organized by what ESLint handles and where it runs out of scope.

Implicit any spreading through your codebase ESLint’s no-explicit-any catches places where you write any. It doesn’t track what happens when any enters your codebase from external sources, such as from response.json(), a third-party library without types, or an untyped import. Once an externally-typed any value enters your code, it propagates silently through property accesses and function calls. ESLint’s no-unsafe-* rules can catch this, but only if you’re using @typescript-eslint/recommended-type-checked, which requires type-aware linting and is significantly less common than the standard recommended config.

async function getUser(id: string) { const res = await fetch(`/api/users/${id}`); const data = await res.json(); // type: any

return data.profile.name; // no error — crashes if profile is undefined } response.json() returns any in the standard lib. Everything downstream is untyped. The compiler accepts any property name and any method call. The bug surfaces at runtime. Qodana tracks how any flows through the program across files. When an any-typed value reaches a code path where a specific shape is assumed, it flags the discrepancy, even if that’s several function calls away from where any entered the codebase.

Adding UserResponse doesn’t fix this. It just moves the lie closer to the crash. Type the boundary instead:

interface UserResponse { profile: { name: string }; }

const data: UserResponse = await res.json(); If the API response shape changes, the type error surfaces at compile time.

Non-null assertions used as shortcuts ESLint’s no-non-null-assertion flags every ! operator uniformly. That works, but many teams disable the rule or add broad exceptions because legitimate uses, like after a runtime check, get flagged alongside the dangerous ones. The signal gets noisy, the rule gets turned off, and the problem disappears from view.

function renderUser(user: User | null) { return `Hello, ${user!.name}`; // crashes at runtime if user is null }

const button = document.querySelector(".submit-btn"); button!.addEventListener("click", handleSubmit); // crashes if element doesn't exist Both examples compile without errors. Both crash under predictable conditions. The ! is often added to silence a type error without fixing the underlying issue.

The correct approach is to handle the null case:

function renderUser(user: User | null) { if (!user) return "Guest"; return `Hello, ${user.name}`; }

const button = document.querySelector(".submit-btn"); if (button) { button.addEventListener("click", handleSubmit); } Qodana surfaces non-null assertions as a separate category in the report. Not every ! is wrong, but seeing them gathered in one place makes it easier to distinguish the legitimate uses from the shortcuts, without having to choose between a noisy rule and no rule at all.

Floating promises ESLint’s @typescript-eslint/no-floating-promises is effective, but it’s a type-aware rule. It requires TypeScript type checking to be enabled in your ESLint config via parserOptions.project. In projects where that’s not configured, or configured only for part of the codebase, the rule silently doesn’t run on uncovered files.

async function onSubmit(data: FormData) { saveToDatabase(data); // Promise<void>, not awaited router.push("/success"); // runs before save completes } TypeScript accepts this without complaint. Calling an async function without await is considered valid syntax, and the return value is discarded. However, this behavior is incorrect: The user sees the success page before the save completes, and any database error is silently swallowed.

async function onSubmit(data: FormData) { await saveToDatabase(data); router.push("/success"); } Qodana’s analysis is type-aware by default across the whole project, without requiring ESLint’s TypeScript integration to be separately configured. Floating promises get flagged consistently regardless of how the project’s ESLint setup is structured.

Unused exports noUnusedLocals in tsconfig catches unused variables within a file. Exported symbols are excluded by design. From the compiler’s perspective, something outside the current file might import them. ESLint’s eslint-plugin-import provides an import/no-unused-modules rule that can detect this, but it requires scanning the entire dependency graph on every lint run and carries significant performance overhead on large codebases. For most projects, it’s not practical to keep it enabled.

// utils/format.ts export function formatCurrency(n: number): string { ... } export function formatPercent(n: number): string { ... } // removed feature, still here export function formatBytes(n: number): string { ... } // never imported anywhere All three pass without a warning. But formatPercent and formatBytes are dead code. They add maintenance surface, slow down refactors, and mislead developers who assume exported symbols are in use.

Detecting this requires whole-project analysis. Qodana builds a reference graph across the entire codebase and tracks every import and re-export. Symbols that appear only as sources, never as import targets, get flagged. Neither tsc nor ESLint can do this.

Duplicated logic across files ESLint doesn’t have native duplication detection. Standalone tools like jscpd exist for this, but they’re not part of your linting pipeline. That means separate setup, separate maintenance, and another thing to remember. The result: logic that gets copied between components or utility files accumulates without anyone flagging it.

// components/UserCard.tsx function formatUserName(user: User): string { if (!user.firstName && !user.lastName) return "Anonymous"; return [user.firstName, user.lastName].filter(Boolean).join(" "); } // components/UserBadge.tsx function getDisplayName(user: User): string { if (!user.firstName && !user.lastName) return "Anonymous"; return [user.firstName, user.lastName].filter(Boolean).join(" "); } This isn’t a style issue. It means bug fixes need to be applied in multiple places, and when they aren’t, behavior diverges silently between the two copies.

Qodana detects duplicated code across files as part of the same analysis pass that surfaces type issues and unused exports. When it appears in the report alongside everything else, it’s harder to deprioritize than a separate tool nobody remembers to run.

Setting up Qodana for your TypeScript project All five issues above are visible in Qodana’s default profile for JavaScript and TypeScript projects. Here is a minimal qodana.yaml to get you started:

version: "1.0" linter: jetbrains/qodana-js:2026.1 bootstrap: npm ci

profile:

name: qodana.recommended failThreshold: 0

exclude:

- name: All

paths:

- dist - node_modules If the first run surfaces hundreds of existing issues, don’t let that block CI adoption. Qodana’s baseline feature captures the current state of the project in a qodana.sarif.json file. Commit it, and from that point on, CI only fails on newly introduced problems. The existing backlog stays visible in the report, but it doesn’t block every PR while you work through it.

Ready to fix common TypeScript issues with Qodana?

Try Qodana and let us know what you think.

Try Qodana Ultimate Plus

We’d like to extend a special thank-you to Qodana developer Lev Liadov for his contribution to this guide.